- 발행일
협업형 여행 계획 플랫폼 TripTune — 2인 팀에서 프런트엔드 전체를 맡은 기록
협업형 여행 계획 플랫폼 TripTune
한눈에 보기
| 항목 | 내용 |
|---|---|
| 서비스 | triptune.co.kr — 함께 여행 계획을 작성하는 웹 서비스 |
| 저장소 | TripTune-Frontend |
| 기간 | 2024.06 ~ 2026.07 (약 25개월) — MVP 6개월(~2025.01) + 이후 운영·개선 |
| 커밋 | 프런트엔드 저장소 504커밋 중 492커밋 (나머지는 dependabot) |
| 팀 구성 | 2명 — 기획 + 프런트엔드 (본인), 디자인 + 백엔드 1명 |
| 기술 스택 | TypeScript, Next.js 15(App Router), React 18, Material-UI, TanStack Query, Zustand, styled-components + CSS Modules, WebSocket/STOMP.js, React DnD |
| 배포·품질 | Netlify (.nvmrc로 Node 22 고정), Jest 단위 테스트, E2E는 Cypress → Playwright 이관 |
| 담당 범위 | 피그마 프로토타입 목업, 프런트엔드 전체 (홈·회원 정보·여행지 탐색·일정 만들기·마이페이지) |
피그마처럼 여럿이 동시에 하나의 일정을 편집하는 애플리케이션을 만들어보고 싶어 시작한 프로젝트입니다. 2인 팀이라 프런트엔드는 전부 제가 맡았고, 기획도 같이 했습니다.
MVP는 6개월 만에 냈지만 거기서 끝나지 않았습니다. 이후 1년 반 동안 도메인을 옮기고(triptune.site → triptune.co.kr), Next.js를 14에서 15로 올리고, 라이트하우스를 잡고, 밀린 보안 경보를 걷어냈어요. 아래 1~6은 MVP 시기의 설계 결정이고, 7절부터는 그 이후에 드러난 것들입니다. 여섯 결정이 같은 무게는 아닙니다 — 하나만 꼽으라면 §1의 React-Query와 Fetch를 역할로 나눈 결정이고, 이유는 맨 아래 결산에 적었습니다.
지표에 대해 — 사이드 프로젝트라 사용자 규모 기반의 정량 지표가 없습니다. 아래 각 항목의 "기대 효과"는 측정한 결과가 아니라 도입 시 의도한 바입니다. 측정하지 않은 걸 성과처럼 적지 않으려고 구분해 표기했습니다.
1. 인증 토큰 자동 갱신 — 작업 도중 로그인이 풀리지 않게
한 일. 커스텀 fetch 함수로 인증이 필요한 API 요청을 일괄 관리했습니다. 토큰이 만료되면 refresh token으로 자동 갱신하고, 갱신도 실패하면 로그인 페이지로 리다이렉트합니다.
왜 이렇게 했나. 이 서비스의 핵심 화면은 여행 일정을 한참 동안 작성하는 화면입니다. 여기서 토큰이 만료돼 입력한 데이터가 날아가면 치명적이에요. 그래서 401 처리를 화면마다 두지 않고 한곳에 모아 사용자 흐름이 끊기지 않게 했습니다.
이때 React-Query와 Fetch를 역할로 나눴습니다.
| 도구 | 쓰는 곳 | 이유 |
|---|---|---|
| React-Query | 무한 스크롤 등 데이터 캐싱이 핵심인 기능 | 캐시·재조회 정책이 도구의 본령 |
| 커스텀 Fetch | 인증 요청처럼 실시간 처리가 중요한 곳 | 캐시가 오히려 방해. 401 인터셉트를 직접 통제해야 함 |
"상태 관리는 하나로 통일한다"는 원칙도 있지만, 여기서는 두 종류의 요청이 서로 다른 것을 원한다고 봤습니다. 인증 요청에 캐시가 끼면 만료 판정이 어긋나니까요.
기대 효과. 재로그인 빈도 감소, 인증·에러 처리 로직 표준화, API 호출부의 중복 제거.
이 구조에 이르기까지의 버그들(로그인 풀림·중복 alert·무한 갱신 루프)과 에러를 enum 4종으로 분류한 설계는 따로 정리했습니다.
2. 검색 자동 완성과 디바운스
한 일. 디바운스 로직을 커스텀 훅으로 만들어 입력 중 발생하는 과도한 서버 요청을 줄이고, 실시간 검색 자동 완성을 붙였습니다.
왜 이렇게 했나. 자동 완성은 입력 반응 속도와 서버 부하가 정면으로 충돌하는 기능입니다. 매 키 입력마다 요청하면 반응은 빠르지만 부하가 크고, 간격을 길게 잡으면 반대가 되고요. 훅으로 뺀 건 이 간격 값을 한 군데서 조정하기 위해서였습니다.
기대 효과. 서버 요청 감소, 검색 UX 개선.
3. Next.js 미들웨어로 요청 경로 사전 검증
한 일. Next.js 미들웨어에서 API 요청 경로를 사전 검증하고, 잘못된 요청은 404 페이지로 리다이렉트했습니다.
왜 이렇게 했나. 잘못된 경로 요청이 서버까지 갔다가 실패하는 건 낭비입니다. 앞단에서 걸러낼 수 있는 건 앞단에서 거르고, 사용자에게도 "없는 페이지"라는 피드백을 명확히 주는 쪽이 나았습니다.
기대 효과. 불필요한 API 요청 감소, 에러 피드백 명확화.
4. Zustand + React-Query 상태 관리
한 일. Zustand와 React-Query를 함께 써서 여행지 탐색 데이터를 관리했습니다. 무한 스크롤은 React-Query의 staleTime·cacheTime으로 캐싱과 호출을 최적화하고, 한 페이지만 유지하는 게 아니라 여러 페이지 간 데이터를 공유·캐싱하도록 설계했습니다.
왜 이렇게 했나. 서버에서 온 데이터와 클라이언트가 들고 있는 상태는 성격이 다릅니다. 전자는 캐시·무효화·재조회가 필요하고(React-Query), 후자는 그냥 가볍게 읽고 쓰면 됩니다(Zustand). 하나로 합치려다 보면 서버 상태를 전역 스토어에 복사해두고 동기화하는 코드가 생기는데, 그게 대체로 버그의 시작이라 처음부터 나눴어요.
페이지 간 캐시 공유는 사용 패턴에서 나왔습니다. 여행지를 탐색하다 상세로 들어갔다 돌아오는 흐름이 잦은데, 돌아올 때마다 목록을 다시 불러오면 스크롤 위치도 데이터도 초기화됩니다.
기대 효과. 중복 요청 감소, 상태 관리 단순화.
5. STOMP 기반 실시간 채팅
한 일. STOMP over WebSocket으로 실시간 메시징을 구현하고, 과거 메시지는 무한 스크롤로 불러오게 했습니다. 연결이 끊겨도 회복되도록 재연결과 하트비트를 설정했습니다.
왜 이렇게 했나. 협업 서비스라 같은 일정을 보는 사람들끼리 대화가 필요했습니다. 순수 WebSocket 대신 STOMP를 얹은 건 구독·발행 개념이 이미 있어서 일정 방(room)별 채널 분리를 직접 만들 필요가 없었기 때문입니다.
재연결과 하트비트는 나중에 붙인 게 아니라 처음부터 넣었습니다. 실시간 기능에서 연결이 끊기는 건 예외가 아니라 정상 동작의 일부라, 끊긴 다음을 설계하지 않으면 그 기능은 미완성이라고 봤어요.
기대 효과. 실시간 소통 가능, 연결 불안정 상황에서의 안정성.
6. 드래그 앤 드롭 일정 편집과 BFF 패턴
한 일. React DnD와 HTML5 Backend로 일정 순서를 드래그로 바꾸는 기능을 만들었습니다. 필요 없는 일정은 삭제 드롭 존으로 빼내게 했고요. 그리고 외부 API의 여행 데이터와 클라이언트 상태(travelRoute)를 병합하는 데 BFF(Backend-for-Frontend) 패턴을 적용했습니다.
왜 BFF였나. 일정 편집 화면은 외부 여행 데이터 + 사용자가 정한 순서(order) 를 합쳐야 화면이 나옵니다. 이 병합을 클라이언트에서 하면 UI 로직과 데이터 가공 로직이 한 컴포넌트에 엉켜요. 순서를 바꿀 때마다 병합을 다시 하고, 그 결과를 렌더에 반영하고, 서버에도 보내야 하니까요.
가공은 서버 쪽으로, 화면은 결과만 받도록 나눴습니다. 이 병합 계층은 제가 구현했습니다. 클라이언트는 "무엇을 보여줄까"만 신경 쓰면 되고요.
DnD 라이브러리로 React DnD를 고른 건 React 생태계 호환과 컴포넌트 기반 설계와의 궁합 때문이었습니다.
기대 효과. 일정 편집의 직관성 향상, UI 로직과 데이터 로직 분리, 클라이언트 복잡도 감소.
7. MVP 이후 1년 반 — 운영에서 드러난 것들
MVP를 낸 뒤가 오히려 배운 게 많았습니다. 각각은 따로 글로 정리했고, 여기서는 무엇이 있었는지만 짚습니다.
| 시기 | 무슨 일 | 글 |
|---|---|---|
| 2025.07 | 여행지 목록↔상세를 오갈 때 북마크 상태가 초기화되던 버그. 인증 상태가 확정되기 전에 요청이 나갔고, 같은 데이터가 캐시 키 두 개로 갈려 있었음 | 인증이 확정되기 전에 쏜 요청 |
| 2026.01~05 | Figma 44프레임과 코드를 전수 대조해 리포트 작성. 22건 REVIEW 중 일부는 아직 미반영 | Figma 44프레임을 코드와 대조하기 |
| 2026.03~04 | 라이트하우스를 잡으러 들어갔다가 루트 레이아웃에 'use client'가 붙어 있는 것을 발견 | 루트 레이아웃에 use client가 붙어 있었다 |
| 2026.07 | Next 14→15 메이저 업그레이드, 밀린 보안 경보 일괄 해소, 죽어 있던 Jest 복구 | 2년 묵은 사이드 프로젝트 되살리기 |
세 번째가 특히 뼈아팠습니다. MVP 때 "Next.js를 쓴다"고 적어놨지만, 루트가 클라이언트 컴포넌트라 App Router의 이점을 하나도 못 쓰고 있었어요. 도구를 도입한 것과 도구가 의도한 대로 쓰는 것은 다른 일이었습니다.
아직 안 치운 것
정직하게, 지금도 남아 있는 것들입니다.
- 선언만 되고 안 쓰이는 의존성 4개 —
react-beautiful-dnd(실제로는react-dnd만 씀),dayjs(date-fns만 씀),react-ga4(라이트하우스 작업에서next/script로 대체했는데 패키지가 남음), 그리고install— 이건npm install install을 친 흔적입니다. - 스타일링이 둘로 갈려 있음 — styled-components 13파일 + CSS Modules 12파일. 초기에 CSS Modules로 가다가 MUI를 붙이며 styled-components가 섞였고, 통일하지 않은 채로 왔습니다.
@types/*두 개가dependencies에 있습니다.
8. 돌아보면
- 2인 팀에서는 "무엇을 안 만들 것인가"가 더 중요했습니다. 6개월 안에 MVP를 내려면 기능을 고르는 게 일의 절반이었어요.
- 도구를 하나로 통일하는 게 항상 옳지는 않았습니다. React-Query와 Fetch를 역할로 나눈 게 이 프로젝트에서 제일 잘한 선택 같아요. "무엇을 캐싱해야 하고 무엇을 캐싱하면 안 되는가"가 기준이었습니다.
- 실시간 기능은 끊긴 다음을 설계해야 완성입니다. 재연결과 하트비트를 나중 과제로 미뤘으면 데모에서만 되는 기능이 됐을 겁니다.
- 도구를 도입한 것과 도구를 의도대로 쓰는 것은 다릅니다. 스택 목록에 "Next.js"라고 적혀 있어도, 루트에
'use client'가 있으면 그건 그냥 리액트 SPA입니다(7절). - MVP 이후가 더 길었습니다. 6개월짜리 프로젝트라고 생각했는데 25개월을 만졌어요. 사이드 프로젝트의 실제 비용은 만드는 데가 아니라 살려두는 데 있었습니다.
- 그리고 아쉬운 점 — 측정을 안 했습니다. 위의 "기대 효과"들은 대부분 의도지 결과가 아니에요. 사이드 프로젝트라 사용자가 적었다는 핑계는 있지만, 요청 수나 렌더 시간처럼 혼자서도 잴 수 있는 건 잴 수 있었습니다. 이후 프로젝트에서는 개선 작업에 숫자를 붙이는 걸 습관으로 삼았습니다.
화면

위 3장과 마지막 1장은 2026년 7월 현재 화면을 다시 캡처했고, 가운데 3장은 로그인이 필요한 화면이라 MVP 시기 캡처를 썼습니다(주소창은 잘라냈어요 — 그때는 triptune.site 도메인이었습니다).